mod config;
mod util;

use crate::cli::{Cli, Method};
use crate::measurement::config::MeasurementConfig;
use crate::measurement::util::{
    output, parse_form, parse_header, parse_header_with_map, parse_json,
};
use anyhow::{anyhow, Result};
use reqwest::blocking::Client;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use std::time::Duration;
use threadpool::ThreadPool;

pub(crate) struct Measurement {
    /// 接口路径，必须
    url: String,

    /// 请求方法
    method: Method,

    /// 表单参数
    form: HashMap<String, Value>,

    /// json参数
    json: HashMap<String, Value>,

    /// 请求数
    pub(crate) requests: u64,

    /// 将请求结果输出到文件
    output: String,

    /// http客户段
    client: Client,

    /// 线程池
    pool: ThreadPool,
}

impl Measurement {
    /// 创建实例
    pub(crate) fn new(cli: Cli) -> Result<Self> {
        if cli.config.is_none() {
            TryFrom::try_from(cli)
        } else {
            let config = MeasurementConfig::read(&cli.config.unwrap())?;
            TryFrom::try_from(config)
        }
    }

    /// 参数校验
    fn verify_params(&self) -> Result<()> {
        if !self.form.is_empty() && !self.json.is_empty() {
            return Err(anyhow!("表单参数和json参数不能同时设置"));
        }
        Ok(())
    }

    /// 组装RequestBuilder
    fn get_builder(&self) -> Result<reqwest::blocking::RequestBuilder> {
        let mut builder = match self.method {
            Method::Get => self.client.get(&self.url),
            Method::Post => self.client.post(&self.url),
            Method::Put => self.client.put(&self.url),
            Method::Delete => self.client.delete(&self.url),
        };

        if !self.form.is_empty() {
            builder = builder.form(&self.form);
        }

        if !self.json.is_empty() {
            builder = builder.json(&self.json);
        }

        Ok(builder)
    }

    /// 请求
    pub(crate) fn request(self) -> Result<u64> {
        self.verify_params()?;
        static COUNTER: AtomicU64 = AtomicU64::new(0);
        static INDEX: AtomicU64 = AtomicU64::new(1);
        let (tx, rx) = mpsc::channel();

        let path = self.output.clone();
        output(path, rx)?;

        for _ in 0..self.requests {
            let sender = tx.clone();
            let builder = self.get_builder()?;
            self.pool.execute(move || {
                let respone = builder.send();
                let index = INDEX.fetch_add(1, Ordering::Relaxed);
                match respone {
                    Ok(resp) => {
                        let code = resp.status();
                        if code.is_success() {
                            let r = resp.text().unwrap();
                            sender.send(format!("Q{}: {}", index, r)).unwrap();
                        } else {
                            COUNTER.fetch_add(1, Ordering::Relaxed);
                            sender
                                .send(format!("Q{}: 请求异常,状态码: {}", index, code.as_u16()))
                                .unwrap();
                        }
                    }
                    Err(err) => {
                        COUNTER.fetch_add(1, Ordering::Relaxed);
                        sender
                            .send(format!("Q{}: 请求异常: {}", index, err))
                            .unwrap();
                    }
                }
            });
        }
        self.pool.join();
        Ok(COUNTER.load(Ordering::Relaxed))
    }
}

impl TryFrom<Cli> for Measurement {
    type Error = anyhow::Error;
    fn try_from(cli: Cli) -> Result<Self> {
        let headers = parse_header(cli.headers)?;
        let form = parse_form(cli.form)?;
        let json = parse_json(cli.json)?;

        let client = Client::builder()
            .timeout(Duration::from_secs(10))
            .default_headers(headers)
            .build()?;

        let pool = threadpool::Builder::new()
            .num_threads(cli.threads as usize)
            .thread_name("pm".to_string())
            .build();
        if cli.url.is_none() {
            return Err(anyhow!("url不能为空"));
        }
        Ok(Measurement {
            url: cli.url.unwrap(),
            method: cli.method,
            client,
            form,
            json,
            pool,
            requests: cli.requests,
            output: cli.output,
        })
    }
}

impl TryFrom<MeasurementConfig> for Measurement {
    type Error = anyhow::Error;
    fn try_from(config: MeasurementConfig) -> Result<Self> {
        let headers = parse_header_with_map(config.headers)?;

        let client = Client::builder()
            .timeout(Duration::from_secs(10))
            .default_headers(headers)
            .build()?;

        let pool = threadpool::Builder::new()
            .num_threads(config.threads as usize)
            .thread_name("pm".to_string())
            .build();

        Ok(Measurement {
            url: config.url,
            method: config.method,
            client,
            form: config.form,
            json: config.json,
            pool,
            requests: config.requests,
            output: config.output,
        })
    }
}
